import inspect
from functools import wraps
from typing import Callable, Iterable
from types import FunctionType
from _others.util import assert_, Err

"""
说明：
互相调用：正常
参数相关：
    1. 默认值参数：
        构造函数和用例函数, 设置参数时必须加上默认值【注意】
        （非用例函数没有限制。）
    2. 可变参数：正常
    3. 与其他类的参数传递：
        可以使用全局变量来传递, 待开发
参数化函数：
    目前只支持用一个参数来接收, 不管单个元素返回的是什么, 
    都传给第一个参数。
用例筛选：
    默认是除了"m"开头以外的所有函数, 其他功能待开发
"""
"""
其他：
    不能通过cls.cases()实例化两次, 第二次返回的self不是实例了
疑问：
    1. 可以直接在把类给pytest运行吗？
        不行。
        它不接受含有构造函数的类，包括基类的构造函数。
        当基类有构造函数时，
        即使把当前类的构造函数删掉，也会存在基类的。
        而我们又必须要继承基类
"""


def params(data_s: Iterable):
    """参数化装饰器"""

    def d02(func):
        setattr(func, ParamsFuncs.params_, data_s)
        return func

    return d02


def filter_funcs(obj: type or dict):
    """筛选用例函数"""
    # obj参数兼容 类对象 和 类字典对象
    if isinstance(obj, dict):
        obj_seq = obj.items()
    else:
        obj_seq = type(obj).__dict__.items()
    # 不需要排除基类的方法, 因为基类的方法虽然也是子类的属性,
    # 但不在子类的命名空间里（__dict__）
    return (
        (key, value)
        for key, value in obj_seq
        if not (key.startswith("m") or  # 私有变量
                not callable(value)  # 不可调用的
                )
    )


def my_partial(func, *args_1, **kwargs_1):
    """
    自定义的partial, 用于解决手动实例化时缺少self的错误
    可以叠加使用, 支持可变参数
    """

    @wraps(func)
    def run(*args_2, **kwargs_2):
        args_all = args_1 + args_2
        kwargs_1.update(kwargs_2)
        return func(*args_all, **kwargs_1)

    return run


class ParamsFuncs:
    """参数化的实现"""
    params_ = "iterable_"

    def __init__(self, attrs):
        # 目前会固定把可迭代对象的元素, 都传给第一个参数（不是self）
        self.attrs_ = dict(self.main(attrs))

    def main(self, attrs):
        """
        循环attrs
        找到符合的, 先循环生成再返回
        不符合的, 直接返回
        """
        for k, v in attrs.items():
            if callable(v) and hasattr(v, self.params_):
                yield from self.gen_func(k, v, getattr(v, self.params_))
            else:
                yield k, v

    def gen_func(self, name, func, iterable_):
        """生成参数化函数并返回"""
        # 获取第二个参数名称
        arg_name = inspect.getfullargspec(func).args[1]
        # 先返回原函数,
        # 再返回其他函数, 其他函数名称从2开始递增
        for i, v in enumerate(iterable_, 1):
            func_ = my_partial(func, **{arg_name: v})
            if i == 1:
                yield name, func_
                continue
            # 修改函数名称, 并作为属性名称返回
            func_.__name__ = f"{name}{i}"
            yield func_.__name__, func_


class Meta1(type):
    """注入修改后的参数化函数"""

    def __new__(mcs, name, bases, attrs, **kwargs):
        # 先忽略无关的基类
        if f"m{name}__ignore" in attrs.keys():
            args = name, bases, attrs
            return type.__new__(mcs, *args, **kwargs)
        # 先判断函数参数的合法性
        mcs.__check_args(attrs)
        # 处理参数化函数
        args = (name, bases, ParamsFuncs(attrs).attrs_)
        return type.__new__(mcs, *args, **kwargs)

    @classmethod
    def __check_args(mcs, attrs):
        # 判断 参数数量 是否等于 默认参数.
        # 1. 先减1, 减去去self参数,
        # 2. 如果是参数化的函数, 再减1
        # 3. 最后：先判断默认参数是否为真, 最后再比较数量
        for _, func in filter_funcs(attrs):
            inspect_func = inspect.getfullargspec(func)
            len_args = len(inspect_func.args)
            len_args -= 1
            if hasattr(func, ParamsFuncs.params_):
                len_args -= 1
            if len_args > 0:
                defaults = inspect_func.defaults
                assert_(defaults and len_args == len(defaults),
                        Err.args_err_msg, func.__qualname__)


class CasesObj:
    def __init__(self, obj):
        self.obj = obj

    def __iter__(self):
        return self

    def __next__(self) -> FunctionType:
        # 用于声明返回值, 方便后续的属性提示.
        # (__iter__不能单个元素, 而__next__可以)
        return next(self.obj)

    def __str__(self):
        cases = [_.__name__ for _ in self.obj]
        return f"名称：{cases} \n数量: {len(cases)}"

    def __call__(self, *args, **kwargs):
        pass


class CollectCases(metaclass=Meta1):
    """收集用例"""
    __ignore = "meta"

    # 1.实例化时能正常的运行
    #   打印实例时, 会输出用例
    #   迭代实例时, 会迭代用例
    # 2.不实例化, 使用类方法能返回所有用例
    #  （作用：不会在被pytest导入时就实例化,
    #    而是实际运行用例时, 才运行实例化的代码）
    #   注意: 不能在打印的同时又迭代, 这样会导致循环两次, 而这种方式不能重复实例化

    @classmethod
    def cases(cls):
        """收集用例的启动函数"""
        _1 = cls.__repl_init()  # 替换init,并取出
        _2 = cls()  # 预先实例化
        _3 = cls.__bind_self  # 过滤函数, 并绑定self参数
        _4 = CasesObj  # 返回可以打印的 迭代器对象
        return _4(_3(_2, _1))

    @classmethod
    def __repl_init(cls):
        def init_(*args, **kwargs): pass

        # 原来的init与空init的交换, 再改名
        if not hasattr(cls, "_init"):
            cls.__init__, cls._init = init_, cls.__init__
            cls._init.__name__ = "init"
        # 实例化空的init
        return cls._init

    @classmethod
    def run_all(cls):
        # 实例对象不能调用, 可能会重复实例化
        for case in cls.cases():
            case()

    def __bind_self(self, init_: Callable = None):
        """生成用例函数"""
        # 绑定self参数、控制顺序
        # 最初传入的必须是self, 而不能是cls。因为需要给其他函数绑定self
        if init_:  # 先返回构造函数
            yield my_partial(init_, self)
        # 再返回其他
        attrs_ = filter_funcs(self)
        for key, value in attrs_:
            yield my_partial(value, self)

    def __str__(self):
        cases = [i.__name__ for i in self.__bind_self()]
        msg = f"{cases} \n数量: {len(cases)}"
        return msg

    def __len__(self):
        return sum(1 for _ in self.__bind_self())


class A(CollectCases):
    def __init__(self):
        pass

    @params(range(1, 4))
    def f1(self, data):
        print(data)


# for i in A.cases():
#     print(id(i))
